package cn.dansj.common.utils.redis.lock;

import cn.dansj.common.utils.bean.DBConnection;
import cn.dansj.common.utils.lock.AbstractLock;
import cn.dansj.common.utils.lock.AutoRegisterLock;
import cn.dansj.common.utils.lock.LockException;
import cn.dansj.common.utils.redis.RedisUtils;
import cn.dansj.common.utils.redis.StringRedisTemplate;
import cn.dansj.common.utils.spring.context.ApplicationContextProvider;
import cn.dansj.common.utils.transfer.AssertUtils;
import cn.dansj.common.utils.transfer.RandomUtils;
import cn.dansj.common.utils.transfer.Verification;
import org.springframework.data.redis.RedisConnectionFailureException;
import org.springframework.data.redis.core.script.DefaultRedisScript;

import java.util.Collections;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.TimeUnit;

@AutoRegisterLock("redis")
public class RedisLock extends AbstractLock {
    private static final String unlockScript = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return -1 end";

    private final StringRedisTemplate redisTemplate;

    private static final Long success = 1L;
    private static final Long error = 0L;
    private static final Long notExist = -1L;
    /**
     * 唯一索引,解锁时比对锁内容,防止解锁别人的锁
     */
    private final String token;
    /**
     * 续租进程的续租时间 second
     */
    private final int expandExpireTime = 30;

    private boolean debug = true;

    private boolean locked = false;
    /**
     * 续租进程的循环间隔时间 milliSecond
     */
    private long period = 10000;

    private ExpandExpire expandExpire;

    public RedisLock(String lockName, long tryGetLockWaitMaxMs, long lockExpireTimeMs, boolean autoLock, DBConnection dbConnection) throws LockException {
        super(lockName, tryGetLockWaitMaxMs, lockExpireTimeMs, autoLock, dbConnection);
        if (dbConnection != null) {
            this.redisTemplate = RedisUtils.createRedisTemplate(dbConnection);
        } else {
            this.redisTemplate = ApplicationContextProvider.getBean(StringRedisTemplate.class);
        }
        this.token = RandomUtils.sequence();
        if (autoLock) lock();
    }

    public RedisLock(String lockName, long tryGetLockWaitMaxMs, DBConnection dbConnection) throws LockException {
        this(lockName, tryGetLockWaitMaxMs, 0, true, dbConnection);
    }

    public RedisLock(String lockName, DBConnection dbConnection) throws LockException {
        this(lockName, Integer.MAX_VALUE, 0, true, dbConnection);
    }

    public RedisLock(String lockName, long tryGetLockWaitMaxMs, long lockExpireTimeMs) throws LockException {
        this(lockName, tryGetLockWaitMaxMs, lockExpireTimeMs, true, null);
    }

    public RedisLock(String lockName, long tryGetLockWaitMaxMs, long lockExpireTimeMs, boolean autoLock) throws LockException {
        this(lockName, tryGetLockWaitMaxMs, lockExpireTimeMs, autoLock, null);
    }

    public RedisLock(String lockName, long tryGetLockWaitMaxMs) throws LockException {
        this(lockName, tryGetLockWaitMaxMs, 0, true, null);
    }

    public RedisLock(String lockName) throws LockException {
        this(lockName, Integer.MAX_VALUE, 0, true, null);
    }

    public RedisLock(String lockName, boolean autoLock) throws LockException {
        this(lockName, Integer.MAX_VALUE, 0, autoLock, null);
    }

    public long getPeriod() {
        return period;
    }

    public void setPeriod(long period) {
        if (period >= expandExpireTime * 1000) {
            throw new IllegalArgumentException(String.format("period:%s, expireTime:%s, period must less than expireTime", period, expandExpireTime * 1000));
        }
        this.period = period;
    }

    public boolean isDebug() {
        return debug;
    }

    public void setDebug(boolean debug) {
        this.debug = debug;
    }

    private void debugLogger(String logInfo, Object... arguments) {
        if (debug) {
            logger.debug(logInfo, arguments);
        } else {
            logger.info(logInfo, arguments);
        }
    }

    public boolean lock() throws LockException {
        if (locked) {
            return false;
        }

        //获取开始时间,用于比较后面等待是否超时
        long startTime = System.currentTimeMillis();
        boolean isSuccess;
        do {
            //尝试获取锁,超时退出等待
            isSuccess = tryLock();
            if (!isSuccess) {
                if ((System.currentTimeMillis() - startTime) >= tryGetLockWaitMaxMs)
                    break;
                try {
                    debugLogger("try get lock:{}, wait lock released", lockName);
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    return false;
                }
            }
        }
        while (!isSuccess);
        if (!isSuccess) {
            throw new LockException(String.format("You can't do it,because another has get the lock(%s).", lockName));
        }

        //当key不过期时,启动续租进程,每隔一段时间重新设置key的有效期
        if (lockExpireTimeMs == 0) {
            debugLogger("start expand thread for lock:{}", lockName);
            expandExpire = new ExpandExpire(redisTemplate, lockName, token, expandExpireTime, period);
            //设置为守护进程,lock结束后结束续租进程
            expandExpire.setDaemon(true);
            expandExpire.start();
        } else {
            debugLogger("lock:{} has expire time({}ms), no need start expand thread", lockName, lockExpireTimeMs);
        }

        locked = true;

        return true;
    }

    private boolean tryLock() {
        //过期时间为空则表示不过期,使用默认过期时间,由续租进程进程续期
        long expire = lockExpireTimeMs == 0 ? expandExpireTime * 1000 : lockExpireTimeMs;
        try {
            //往redis写入key
            AssertUtils.notNull(token, "lock token is null");
            Boolean result = redisTemplate.opsForValue().setIfAbsent(lockName, token, expire, TimeUnit.MILLISECONDS);
            return Boolean.TRUE.equals(result);
        } catch (IllegalStateException illegalStateException) {
            final String message = illegalStateException.getMessage();
            if (message.equals("LettuceConnectionFactory is STOPPING") || message.equals("Unable to connect to Redis")) {
                throw illegalStateException;
            }
            logger.error("try lock failed({}) by error:{}.", lockName, message);
        } catch (RedisConnectionFailureException redisConnectionFailureException) {
            logger.error("lock ({}) connection error:{}", lockName, redisConnectionFailureException.getMessage());
            throw redisConnectionFailureException;
        } catch (Exception e) {
            logger.error("try lock failed({}) by error:{}", lockName, e.getMessage());
        }
        return false;
    }

    public boolean unlock() {
        if (expandExpire != null) {
            expandExpire.setReadyUnlock(true);
        }
        if (!Verification.checkNull(token)) {
            List<String> key = Collections.singletonList(lockName);
            try {
                DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
                redisScript.setScriptText(unlockScript);
                redisScript.setResultType(Long.class);
                Long unlock = redisTemplate.execute(redisScript, key, token);
                if (success.equals(unlock)) {
                    debugLogger("{} unlocked successfully", lockName);
                    locked = false;
                    return true;
                } else if (error.equals(unlock)) {
                    logger.warn("{} unlocking failed", lockName);
                    return false;
                } else if (notExist.equals(unlock)) {
                    logger.warn("lock:{} has been changed, can't unlock", lockName);
                    return false;
                }
            } catch (Exception e) {
                logger.error("release lock:{} failed -> {}", lockName, e.getMessage());
            } finally {
                if (expandExpire != null) {
                    debugLogger("close expand expire thread");
                    expandExpire.cancelPurge();
                }
            }
        }
        return false;
    }

    class ExpandExpire extends Thread {
        private static final String expandScript = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('expire', KEYS[1],ARGV[2]) else return -1 end";

        private final StringRedisTemplate redisTemplate;
        private final String lockName;
        private final String token;

        private final int expireTime;
        private final long period;

        private int errNum = 0;

        private volatile Timer timer = new Timer();

        private volatile boolean readyUnlock = false;

        public ExpandExpire(StringRedisTemplate redisTemplate, String lockName, String token, int expireTime, long period) {
            this.redisTemplate = redisTemplate;
            this.lockName = lockName;
            this.token = token;
            this.expireTime = expireTime;
            this.period = period;
        }

        public void setReadyUnlock(boolean readyUnlock) {
            this.readyUnlock = readyUnlock;
        }

        @Override
        public void run() {
            if (timer == null) {
                return;
            }
            try {
                timer.schedule(new TimerTask() {
                    @Override
                    public void run() {
                        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
                        redisScript.setScriptText(expandScript);
                        redisScript.setResultType(Long.class);
                        List<String> key = Collections.singletonList(lockName);

                        try {
                            Long expire = redisTemplate.execute(redisScript, key, token, String.valueOf(expireTime));

                            if (success.equals(expire)) {
                                debugLogger("expand lock:{} expire succeed", lockName);
                            } else if (error.equals(expire)) {
                                logger.warn("expand lock:{} expire failed", lockName);
                            } else if (notExist.equals(expire) && !readyUnlock) {
                                logger.warn("lock:{} has been changed, expand thread exit", lockName);
                                cancelPurge();
                            }
                        } catch (Exception e) {
                            if (errNum > 3) {
                                logger.error("expand lock:{} expire failed by max test", lockName);
                            }
                            logger.warn("expand lock:{} expire failed {}", lockName, errNum);
                            errNum++;
                        }
                    }
                }, period, period);
            } catch (Exception exception) {
                if (!(exception instanceof IllegalStateException)) {
                    logger.error("error:", exception);
                }
                cancelPurge();
            }
        }

        public void cancelPurge() {
            if (timer != null) {
                timer.cancel();
                timer.purge();
                timer = null;
            }
        }
    }
}
